﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;

namespace BorgExt.DynamicProxy
{
    public static class DynamicProxy
    {
        private const int OFFSET_FOR_THIS_POINTER = 1;

        private static void AssignParametersToParameterArray(ILGenerator ilGenerator,
            ParameterInfo[] ifaceMethodParams, LocalBuilder parameterArray)
        {
            for (var i = 0; i < ifaceMethodParams.Length; i++)
            {
                ilGenerator.EmitLoadLocal(parameterArray.LocalIndex);
                ilGenerator.EmitLoadConst(i);
                ilGenerator.EmitLoadArg(ifaceMethodParams[i].Position + 1);

                if (ifaceMethodParams[i].ParameterType.IsPrimitive)
                    ilGenerator.Emit(OpCodes.Box, ifaceMethodParams[i].ParameterType);

                ilGenerator.Emit(OpCodes.Stelem_Ref);
            }
        }

        private static void CallInvokationHandler(ILGenerator ilGenerator,
            FieldBuilder handlerField, MethodInfo ifaceMethod, LocalBuilder parameterArray)
        {
            ilGenerator.EmitLoadArg(0);
            ilGenerator.Emit(OpCodes.Ldfld, handlerField);
            ilGenerator.EmitLoadArg(0);
            ilGenerator.Emit(OpCodes.Ldtoken, ifaceMethod);

            ilGenerator.EmitCall(OpCodes.Call, typeof(MethodBase).
                GetMethod("GetMethodFromHandle", new[] { typeof(RuntimeMethodHandle) }), null);

            ilGenerator.EmitLoadLocal(parameterArray.LocalIndex);
            ilGenerator.EmitCall(OpCodes.Callvirt, typeof(InvocationHandler).GetMethod("Invoke"), new Type[0]);
        }

        private static void DefineConstructor(System.Reflection.Emit.TypeBuilder proxyClass, FieldBuilder handlerField)
        {
            var builder = proxyClass.DefineConstructor(
                MethodAttributes.RTSpecialName |
                MethodAttributes.SpecialName |
                MethodAttributes.HideBySig |
                MethodAttributes.Public,
                CallingConventions.Standard,
                new[] { typeof(InvocationHandler) });

            builder.DefineParameter(1, ParameterAttributes.In, "handler");

            var iLGenerator = builder.GetILGenerator();

            iLGenerator.EmitLoadArg(0);
            iLGenerator.Emit(OpCodes.Call, typeof(object).GetConstructor(new Type[0]));
            iLGenerator.EmitLoadArg(0);
            iLGenerator.EmitLoadArg(1);
            iLGenerator.Emit(OpCodes.Stfld, handlerField);
            iLGenerator.Emit(OpCodes.Ret);
        }

        private static void DefineEvents(System.Reflection.Emit.TypeBuilder proxyClass,
            IEnumerable<Type> interfaces, Dictionary<MethodInfo, MethodBuilder> methods)
        {
            foreach (var type in interfaces)
            {
                foreach (var eventInfo in type.GetEvents())
                {
                    var name = GetFullTypeName(type) + eventInfo.Name;
                    var builder = proxyClass.DefineEvent(
                        name, eventInfo.Attributes, eventInfo.EventHandlerType);

                    builder.SetAddOnMethod(methods[eventInfo.GetAddMethod()]);
                    builder.SetRemoveOnMethod(methods[eventInfo.GetRemoveMethod()]);
                }
            }
        }

        private static FieldBuilder DefineHandlerField(System.Reflection.Emit.TypeBuilder proxyClass)
        {
            return proxyClass.DefineField("handler", typeof(InvocationHandler), FieldAttributes.Private);
        }

        private static LocalBuilder DefineLocalParameterArray(ILGenerator ilGenerator, MethodInfo ifaceMethod)
        {
            var builder = ilGenerator.DeclareLocal(typeof(object[]));

            ilGenerator.EmitLoadConst(ifaceMethod.GetParameters().Length);
            ilGenerator.Emit(OpCodes.Newarr, typeof(object));
            ilGenerator.EmitStoreLocal(builder.LocalIndex);

            return builder;
        }

        private static MethodBuilder DefineMethod(System.Reflection.Emit.TypeBuilder proxyClass, MethodInfo ifaceMethod)
        {
            var name = GetFullTypeName(ifaceMethod.DeclaringType) + ifaceMethod.Name;

            var parameterTypes =
                (from info in ifaceMethod.GetParameters()
                 select info.ParameterType).ToArray<Type>();

            var methodInfoBody = proxyClass.DefineMethod(
                name,
                MethodAttributes.NewSlot |
                MethodAttributes.HideBySig |
                MethodAttributes.Virtual |
                MethodAttributes.Final |
                MethodAttributes.Private,
                CallingConventions.Standard,
                ifaceMethod.ReturnType,
                parameterTypes);

            proxyClass.DefineMethodOverride(methodInfoBody, ifaceMethod);
            return methodInfoBody;
        }

        private static void DefineMethodBody(FieldBuilder handlerField, MethodInfo interfaceMethod, MethodBuilder proxyMethod)
        {
            var iLGenerator = proxyMethod.GetILGenerator();
            var parameterArray = DefineLocalParameterArray(iLGenerator, interfaceMethod);
            
            AssignParametersToParameterArray(iLGenerator, interfaceMethod.GetParameters(), parameterArray);
            CallInvokationHandler(iLGenerator, handlerField, interfaceMethod, parameterArray);
            UnboxReturnValue(iLGenerator, proxyMethod);
            iLGenerator.Emit(OpCodes.Ret);
        }

        private static Dictionary<MethodInfo, MethodBuilder> DefineMethods(
            System.Reflection.Emit.TypeBuilder proxyClass, IEnumerable<Type> interfaces, FieldBuilder handlerField)
        {
            var dictionary = new Dictionary<MethodInfo, MethodBuilder>();

            foreach (var type in interfaces)
            {
                foreach (var methodInfo in type.GetMethods())
                {
                    var proxyMethod = DefineMethod(proxyClass, methodInfo);

                    DefineParameters(proxyMethod, methodInfo);
                    DefineMethodBody(handlerField, methodInfo, proxyMethod);

                    dictionary.Add(methodInfo, proxyMethod);
                }
            }

            return dictionary;
        }

        private static Dictionary<MethodInfo, MethodBuilder> DefineMethods(
            System.Reflection.Emit.TypeBuilder proxyClass, IEnumerable<Type> interfaces,
            Type wrapeeType, FieldBuilder handlerField)
        {
            var dictionary = new Dictionary<MethodInfo, MethodBuilder>();

            foreach (var type in interfaces)
            {
                foreach (var methodInfo in type.GetMethods())
                {
                    var proxyMethod = DefineMethod(proxyClass, methodInfo);

                    DefineParameters(proxyMethod, methodInfo);
                    DefineMethodBody(handlerField, methodInfo, proxyMethod);

                    dictionary.Add(methodInfo, proxyMethod);
                }
            }

            foreach (var methodInfo in wrapeeType.
                GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance))
                Console.WriteLine(methodInfo.GetBaseDefinition());

            return dictionary;
        }

        private static void DefineParameters(MethodBuilder proxyMethod, MethodInfo ifaceMethod)
        {
            var parameters = ifaceMethod.GetParameters();
            var builders = new ParameterBuilder[parameters.Length];

            for (var i = 0; i < parameters.Length; i++)
            {
                var paramInfo = parameters[i];
                builders[i] = proxyMethod.DefineParameter(i + 1, paramInfo.Attributes, paramInfo.Name);
            }
        }

        private static void DefineProperties(System.Reflection.Emit.TypeBuilder proxyClass,
            IEnumerable<Type> interfaces, Dictionary<MethodInfo, MethodBuilder> methods)
        {
            foreach (var type in interfaces)
            {
                var properties = type.GetProperties();

                foreach (var property in properties)
                {
                    var name = GetFullTypeName(type) + property.Name;
                    var propertyBuilder =
                        proxyClass.DefineProperty(
                            name,
                            property.Attributes,
                            property.PropertyType,
                            (from info in
                                 property.GetIndexParameters()
                             select info.ParameterType).ToArray<Type>());

                    propertyBuilder.SetGetMethod(methods[property.GetSetMethod(true)]);
                    propertyBuilder.SetSetMethod(methods[property.GetGetMethod(true)]);
                }
            }
        }

        private static System.Reflection.Emit.TypeBuilder GetClassImplementing(Type[] interfaces)
        {
            var currentDomain = AppDomain.CurrentDomain;
            var proxyTypeName = GetProxyTypeName(interfaces);
            var asmName = new AssemblyName { Name = proxyTypeName };

            return currentDomain.DefineDynamicAssembly(
                asmName,
                AssemblyBuilderAccess.RunAndSave).DefineDynamicModule(
                    "ProxyModule",
                    "Proxy.dll",
                    true).DefineType(
                        proxyTypeName,
                        TypeAttributes.Public,
                        typeof (object),
                        interfaces);
        }

        private static System.Reflection.Emit.TypeBuilder GetClassWithSameInterface(Type wrapeeType)
        {
            var interfaces = wrapeeType.GetInterfaces();
            var classImplementing = GetClassImplementing(interfaces);
            var handlerField = DefineHandlerField(classImplementing);

            DefineConstructor(classImplementing, handlerField);
            DefineMethods(classImplementing, interfaces, wrapeeType, handlerField);

            return classImplementing;
        }

        private static string GetFullTypeName(Type ifaceType)
        {
            var fullName = ifaceType.Namespace != null
                ? ifaceType.Namespace + "."
                : "";
            
            return (fullName + ifaceType.Name + ".");
        }

        public static object GetProxy(Type wrapeeType, InvocationHandler handler)
        {
            return GetProxyClass(wrapeeType).
                GetConstructor(new[] { typeof(InvocationHandler) }).Invoke(new object[] { handler });
        }

        public static object GetProxy(Type[] interfaces, InvocationHandler handler)
        {
            return GetProxyClass(interfaces).CreateType().
                GetConstructor(new [] { typeof(InvocationHandler) }).Invoke(new object[] { handler });
        }

        private static Type GetProxyClass(Type wrapeeType)
        {
            var classWithSameInterface = GetClassWithSameInterface(wrapeeType);
            var handlerField = DefineHandlerField(classWithSameInterface);

            DefineConstructor(classWithSameInterface, handlerField);

            return classWithSameInterface.CreateType();
        }

        private static System.Reflection.Emit.TypeBuilder GetProxyClass(Type[] interfaces)
        {
            var classImplementing = GetClassImplementing(interfaces);
            var handlerField = DefineHandlerField(classImplementing);
            
            DefineConstructor(classImplementing, handlerField);
            
            var methods = DefineMethods(classImplementing, interfaces, handlerField);

            DefineProperties(classImplementing, interfaces, methods);
            DefineEvents(classImplementing, interfaces, methods);

            return classImplementing;
        }

        private static string GetProxyTypeName(Type[] interfaces)
        {
            var builder = new StringBuilder("Proxy");

            foreach (var type in interfaces)
                builder.AppendFormat("_{0}", type.Name);

            builder.AppendFormat("_{0}", Guid.NewGuid().ToString().Replace('-', '_'));
            
            return builder.ToString();
        }

        private static void UnboxReturnValue(ILGenerator ilGenerator, MethodBuilder newMethod)
        {
            if (newMethod.ReturnType == typeof (void))
                ilGenerator.Emit(OpCodes.Pop);
            
            else if (newMethod.ReturnType.IsPrimitive)
                ilGenerator.Emit(OpCodes.Unbox_Any, newMethod.ReturnType);
        }
    }
}

